延續昨天討論到執行緒安全的問題,今天要介紹可以使用 synchronized 的關鍵字去保護變數或是方法的執行。當有多個執行緒要存取同一個共享變數的值時,如果此變數在使用前加上 synchronized 關鍵字,它就可以確保同一個時間內只會有一個執行緒去存取這個共享變數的值,其它的執行緒會在等待的狀態內,等待沒有其它的執行緒執行這個共享變數,才有機會進入執行存取共享變數的動作。
舉一個日常生活的例子:
如果廁所只有一間,如果有人想要上廁所這時就要先看廁所是否是使用中或是未使用。如果是使用中狀態的話其他人只能排隊等待其它人用完之後才能進去使用,如果是未使用的話直接就可以進入廁所使用。在這裡的廁所就可以把它當作是共享變數,排隊的人就可以當作是執行緒的執行。
昨天發現 ArrayList 是執行緒不安全的 Collection,今天把 ArrayList 要呼叫 add 方法前時使用synchronized 關鍵字,去保護在同時只能一個執行緒去存取該變數,確認執行結果是正確的, Sample Code 如下:
import java.util.List;
public class ArrayListThreadTest1 implements Runnable {
private List<String> list;
public ArrayListThreadTest1(List<String> list) {
this.list = list;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
synchronized(list) {
this.list.add("a" + i);
}
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
}
上面的程式主要是在執行緒執行到 run 時,會去使用 synchronized關鍵字去保護 list 的共享變數,確保在多執行緒執行時執行的結果不會被破壞掉。
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String args[]) throws Exception {
List<String> list = new ArrayList<String>();
Thread thread1 = new Thread(new ArrayListThreadTest1(list));
Thread thread2 = new Thread(new ArrayListThreadTest1(list));
Thread thread3 = new Thread(new ArrayListThreadTest1(list));
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println("list size is " + list.size());
}
}
上面的程式是主程式,主要會去呼叫 3 個執行緒執行,這個程式寫法和昨天類似,只是在實作 run 方法加上 synchronized 去保護 list,以下是執行結果:
list size is 30
我們會發現執行結果是 30 時,跟我們預期的結果是一樣的。經過測試多次之後輸出結果一樣是 30。因此如果我們在使用執行緒不安全的 class 時,要記得使用 synchronized 去保護變數,不要被破壞。但是使用 synchronized 也會有一個缺點就是執行緒之間要等待,所以有可以會消耗掉一點執行的效能,為了運算結果的正確性,這是一定要的犧牲。或者是另外去找一些有支援執行緒安全的 Collection,這樣就可以不用加上 synchronized 去保護變數。